package ads.client;

import java.net.URI;
import java.rmi.NoSuchObjectException;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.UnicastRemoteObject;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;

import org.apache.log4j.Logger;

import ads.exception.PlayerNotConnectedException;
import ads.exception.PlayingGameException;
import ads.exception.ServerRemoteException;
import ads.remote.ClientInterface;
import ads.remote.PeerInterface;
import ads.remote.ServerInterface;
import ads.util.ClientConfig;
import ads.util.GameInfo;
import ads.util.PlayerInfo;
import at.falb.games.alcatraz.api.Alcatraz;
import at.falb.games.alcatraz.api.Player;
import at.falb.games.alcatraz.api.Prisoner;
import at.falb.games.alcatraz.impl.GUIFrame;

public class ClientImpl implements ClientInterface, PeerInterface {

	/**
	 * 
	 */
	private static final long serialVersionUID = -457796100168536349L;
	private static final Logger log = Logger.getLogger(ClientImpl.class
			.getName());
	private ClientConfig config;
	private ServerInterface server;
	private Player player;
	private PlayerInfo playerInfo;
	private GameInfo gameInfo;
	private int myPlayerId;
	private GameBoard gameBoard;
	private ArrayList<PlayerInfo> playersInfo;

	public ClientImpl(ClientConfig config) throws ServerRemoteException {
		super();
		this.config = config;
		initialize();
	}

	private void initialize() throws ServerRemoteException {
		if (System.getSecurityManager() == null) {
			System.setSecurityManager(new SecurityManager());
		}
		try {
			UnicastRemoteObject.exportObject(this, config.getClientPort());
			log.debug("client export himself from callback");

			Registry registry = LocateRegistry.getRegistry(config
					.getRegstryHost(), config.getRegstryPort());
			log.debug("LocateRegistry.getRegistry:" + config.getRegstryHost()
					+ ":" + config.getRegstryPort());
			server = (ServerInterface) registry.lookup(config.getServerName());
			log.debug("lookup Server " + config.getServerName()
					+ ",server stub:" + server);
		} catch (RemoteException e) {
			log.debug(e);
			throw new ServerRemoteException(
					"RemoteException, Failed to initilize rmi.", e);
		} catch (NotBoundException e) {
			log.debug(e);
			throw new ServerRemoteException(
					"NotBoundException, Failed to lookup server.", e);
		}
	}

	/**
	 * 
	 * @param username
	 * @throws PlayingGameException
	 */
	public void login(String username) throws PlayingGameException {
		player = new Player(username.hashCode());
		player.setName(username);
		try {
			server.register(player, this);
		} catch (RemoteException e) {
			log.error("RemoteException,login Failed", e);
			throw new PlayingGameException("RemoteException,Failed to login", e);
		} catch (PlayingGameException e) {

		}
		// successful login create PlayerInfo
		playerInfo = new PlayerInfo(player, this);
	}

	/**
	 * 
	 * @param username
	 * @throws PlayingGameException
	 */
	public void relogin(String username) throws PlayingGameException {
		log.debug("relogin for player:" + username);

		player = new Player(username.hashCode());
		player.setName(username);
		try {
			server.relogin(player, this);
		} catch (RemoteException e) {
			log.error("RemoteException,login Failed", e);
			throw new PlayingGameException("RemoteException,Failed to login", e);
		} catch (PlayingGameException e) {
			log.error("PlayinGameException,login Failed", e);
		}

		// successful relogin create PlayerInfo
		playerInfo = new PlayerInfo(player, this);
		log.debug("playerinfo after relogin:" + playerInfo);

		// request server for recovery
		try {
			server.requestRecovery(playerInfo);
		} catch (RemoteException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}

	@Override
	public void forwardConnection(URI uri) throws RemoteException {
		log.info("forward connection and connect with new server" + uri);
		Registry registry = LocateRegistry.getRegistry(uri.getHost(), uri
				.getPort());
		log.debug("LocateRegistry.getRegistry:" + uri);

		String serverName = uri.getPath();
		if (serverName.charAt(0) == '/') {
			serverName = serverName.substring(1, serverName.length());
		}
		if (serverName.charAt(serverName.length() - 1) == '/') {
			serverName = serverName.substring(0, serverName.length() - 1);
		}
		try {
			server = (ServerInterface) registry.lookup(serverName);
			log.debug("lookup Server " + serverName + ",server stub:" + server);
		} catch (NotBoundException e) {
			e.printStackTrace();
			log.error(e);
		}
	}

	@Override
	public void beginGame(GameInfo game) throws RemoteException {

		log.debug("begin game" + game.toString());

		// initialize the game for each cleint
		playersInfo = game.getPlayers();
		Iterator<PlayerInfo> i = playersInfo.iterator();
		PlayerInfo tmpPlayerInfo;
		Player tmpPlayer;
		// my player details
		myPlayerId = -1;
		int turn = -1;
		while (i.hasNext()) {
			tmpPlayerInfo = i.next();
			tmpPlayer = tmpPlayerInfo.getPlayer();
			turn++;
			if (this.player.equals(tmpPlayer)) {
				myPlayerId = turn;
			}
		}

		// start game
		log.debug("initializing game no Of players:" + game.getCountOfPlayer()
				+ ", playerId:" + myPlayerId + ", name:"
				+ this.player.getName());
		// new GameBoard(game, myPlayerId, this.player.getName(),
		// game.getCountOfPlayer());

		// start GameBoard instance with moveID = 1
		gameBoard = GameBoard.getInstance(game, myPlayerId, this.player
				.getName(), 1);
		/*
		 * Every player in a new threadserver while (i.hasNext()) {
		 * tmpPlayerInfo = i.next(); if
		 * (this.player.equals(tmpPlayerInfo.getPlayer())) // start this server
		 * thread new StartPlayerAsServerThread(tmpPlayerInfo.getPlayer()
		 * .getName(), this).run(); } log.debug("connection to each peer
		 * called"); // make all players connect/register to each other i =
		 * playersInfo.iterator(); while(i.hasNext()){ tmpPlayerInfo = i.next();
		 * if (this.player.equals(tmpPlayerInfo.getPlayer()))
		 * connectToPeers(this.playerInfo, playersInfo); // else } /*
		 * connectToPeers(this, game.getPlayers()); //connect to other players
		 * for (PlayerInfo playerInfo : game.getPlayers()) { try { new
		 * ConnectToPlayersThread(game).run();
		 * //playerInfo.getStub().beginGame(game); } catch (Exception e) {
		 * log.fatal("Error connecting to other players:" + playerInfo, e);
		 * e.printStackTrace(); } }
		 */

	}

	@Override
	public void recovery(GameInfo game) throws RemoteException {
		log.debug("recovery game:" + game.toString());

		boolean recovery = true;
		// update all players with new game(stub) information
		ArrayList<PlayerInfo> oldPlayersList = null;
		gameInfo = game;

		// get player array from the new game info
		ArrayList<PlayerInfo> newPlayersInfo = game.getPlayers();
		Iterator<PlayerInfo> i = newPlayersInfo.iterator();
		PlayerInfo tmpPlayerInfo;
		PeerInterface player;
		String currentPlayerName;

		while (i.hasNext()) {
			try {
				tmpPlayerInfo = i.next();
				currentPlayerName = tmpPlayerInfo.getPlayer().getName();

				if (!currentPlayerName.equals(playerInfo.getPlayer().getName())) {
					log.debug("send new game to:" + tmpPlayerInfo);
					player = (PeerInterface) tmpPlayerInfo.getStub();

					oldPlayersList = player.getPreviousPlayersList();

					// update this players turn list with old list
					playersInfo = oldPlayersList;
					player.sendNewGameInfo(game);
				}

			} catch (Exception e) {
				log.error(e.getMessage());
			}
		}

		log.debug("Offline player ignored");
		// get old turn list
		i = oldPlayersList.iterator();
		int turn = -1;
		while (i.hasNext()) {
			tmpPlayerInfo = i.next();
			turn++;
			if (tmpPlayerInfo.getPlayer().getName().equals(
					this.player.getName())) {
				myPlayerId = turn;
			}

		}

		// get previous moves & previous moveID
		Hashtable<Integer, Move> moveList = null;
		i = newPlayersInfo.iterator();

		while (i.hasNext()) {
			try {
				tmpPlayerInfo = i.next();
				if (!tmpPlayerInfo.getPlayer().getName().equals(
						playerInfo.getPlayer().getName())) {
					log.debug("get list of previous moves from:"
							+ tmpPlayerInfo.getPlayer().getName());
					player = (PeerInterface) tmpPlayerInfo.getStub();
					moveList = player.getMoveList();

				}
			} catch (Exception e) {
				log.error(e.getMessage());
			}
			// above not the best solution & not correct yet
		}

		// restart game
		log.debug("initializing game no Of players:" + game.getCountOfPlayer()
				+ ", playerId:" + myPlayerId + ", name:"
				+ this.player.getName());

		// restarting so the moveID is set correctly
		gameBoard = GameBoard.getInstance(game, myPlayerId, this.player
				.getName(), moveList.size() + 1);

		// run the previous moves on this Gameboard
		Enumeration<Integer> e = moveList.keys();
		log.debug("list of moves");
		Move move;
		Integer moveID;
		while (e.hasMoreElements()) {
			moveID = e.nextElement();
			log.debug("moveID:" + moveID + ", move:" + moveList.get(moveID));

		}
		// the game entity needs to be update on the GameBoard &
		// the previous moves need to be
		gameBoard.doMove(gameInfo, moveList);

	}

	public Hashtable<Integer, Move> getMoveList() throws RemoteException {
		log.debug("get move list from other players");
		Hashtable<Integer, Move> moveList = GameBoard.moveList;

		Enumeration<Integer> e = moveList.keys();
		log.debug("list of moves");
		Move move;
		Integer moveID;
		while (e.hasMoreElements()) {
			moveID = e.nextElement();
			log.debug("moveID:" + moveID + ", move:" + moveList.get(moveID));
			System.out.println(moveList.get(moveID));
		}

		return GameBoard.moveList;
	}

	public List<GameInfo> getAllGame() throws PlayingGameException {
		try {
			return server.listAllGames();
		} catch (RemoteException e) {
			log.error("error by retrive all game from server", e);
			e.printStackTrace();
			throw new PlayingGameException(
					"error by retrive all game from server");
		}
	}

	public GameInfo createGame(Integer count) throws PlayingGameException {
		try {
			gameInfo = server.createGame(player, count);
			log.debug("game created:" + gameInfo);
			return gameInfo;

		} catch (RemoteException e) {
			log.error("RemoteException,error by create game", e);
			throw new PlayingGameException(
					"RemoteException,error by create game");
		} catch (PlayingGameException e) {
			log.error("PlayingGameException,error by create game", e);
			throw new PlayingGameException(
					"PlayingGameException,,error by create game");
		}
	}

	public GameJoinStatus joinGame(GameInfo game) throws PlayingGameException {
		try {
			if (game != null) {
				// check if the creator joins or 2x join again
				if (!game.hasPlayer(playerInfo)) {
					gameInfo = server.joinGame(playerInfo, game.getId());
				} else {
					return GameJoinStatus.GameFull;
				}
			} else {
				return GameJoinStatus.GameNotExist;
			}

		} catch (RemoteException e) {
			log.error("RemoteException,error by join game", e);

		} catch (PlayingGameException e) {
			log.error("PlayingGameException,error by join game", e);

		}
		return GameJoinStatus.Ok;
	}

	@Override
	public void sendMove(Move move) throws RemoteException {
		// TODO Auto-generated method stub
		log.debug("remote call successful to sendMove method on:"
				+ this.player.getName());
		if (!GameBoard.moveList.containsKey(move.getMoveID())) {

			// add received move of other player to local moveList
			GameBoard.moveList.put(move.getMoveID(), move);
			gameBoard = GameBoard.getInstance(gameInfo, myPlayerId, this.player
					.getName(), 1);
			log.debug("moveID:" + move.getMoveID()
					+ ";calling doMove on remote client:"
					+ this.player.getName());
			// gameBoard.doMove(move.getPlayer(), move.getPrisoner(), move
			// .getRowOrCol(), move.getRow(), move.getCol());

			// why does gameInfo send gameinfo wiht only this player??
			// gameBoard.doMove(gameInfo, move);
			gameBoard.doMove(move);

		}

	}

	@Override
	public void RegisterPlayer(String playerName, ClientInterface stub)
			throws RemoteException {
		// TODO Auto-generated method stub

	}

	@Override
	public void connectToPeers(PlayerInfo playerInfo,
			ArrayList<PlayerInfo> players) throws RemoteException {
		// TODO Auto-generated method stub

	}

	@Override
	public void updateCurrentList(ArrayList<Player> playerList)
			throws RemoteException {
		// TODO Auto-generated method stub

	}

	public Player getPlayer() {
		return this.player;
	}

	public GameInfo getGameInfo() {
		return gameInfo;
	}

	@Override
	public void sendNewGameInfo(GameInfo game) throws RemoteException {
		// TODO Auto-generated method stub
		log.debug("share new game:" + game + ":\n info with player:"
				+ this.player.getName());
		log.debug("gameinfo:" + gameInfo + ", newgame:" + game);
		gameInfo = game;

		log.debug("new game info" + gameInfo);

		// update the gameinfo entity on GameBoard
		gameBoard.updateGameInfo(gameInfo);
	}

	@Override
	public ArrayList<PlayerInfo> getPreviousPlayersList()
			throws RemoteException {
		// TODO Auto-generated method stub
		log.debug("get old player list from:" + this.player);

		return playersInfo;
	}

}
